feat: Add permission_ids support to team member profiles and invitations#870
feat: Add permission_ids support to team member profiles and invitations#870bootssecurity wants to merge 14 commits intohexclave:devfrom bootssecurity:dev
Conversation
- Add permission_ids field to team member profile schemas (client/server) - Update team member profiles API to include permission_ids in responses - Modify team member list UI to display roles based on permission_ids - Update team invitation endpoints to handle permission_ids - Add role-permissions endpoint for fetching available permissions - Update documentation with permission_ids examples and type definitions - Add conceptual documentation for role-based team invitations
|
@bootssecurity is attempting to deploy a commit to the Stack Team on Vercel. A member of the Team first needs to authorize it. |
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds role-based permission support for team invitations: invitation payloads can include permission_ids, surfaced in CRUD and member profiles, selectable in the UI, exposed via a new role-permissions endpoint, and granted transactionally when an invitation is accepted. Tests and docs updated. Changes
Sequence Diagram(s)sequenceDiagram
participant Admin as Admin (inviter)
participant UI as Frontend
participant API as Backend API
participant DB as Database
rect rgb(220,245,220)
Note over Admin,API: Create & send invitation with role
Admin->>UI: select role + email
UI->>API: POST /team-invitations/send-code { email, permission_ids: [...] }
API->>DB: persist invitation with permission_ids
API-->>UI: 200 Invitation created
end
rect rgb(220,230,255)
Note over User,API: Recipient accepts invitation
User->>API: POST /team-invitations/accept { code }
API->>API: retryTransaction(start)
API->>DB: create membership (tx)
API->>DB: grant permissions for permission_ids (tx)
API->>API: retryTransaction(commit)
API-->>User: 200 Joined team
end
rect rgb(255,245,210)
Note over UI,API: Display members & roles
UI->>API: GET /team-member-profiles
API->>DB: fetch members + permission_ids (batched)
API-->>UI: members[] with permission_ids
UI->>UI: map permission_ids -> role label
UI-->>Admin: show member with Role badge
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🧰 Additional context used🧬 Code graph analysis (1)apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (3)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Review by RecurseML🔍 Review performed on 6a3459e..cf8e76b
✅ Files analyzed, no issues (4)• ⏭️ Files skipped (low suspicion) (14)• |
There was a problem hiding this comment.
Greptile Summary
This PR implements role-based team invitations and permission management for Stack Auth, extending the existing team management system with granular permission control. The changes add permission_ids support across the entire stack - from database schemas and API endpoints to client interfaces and UI components.
Key architectural changes include:
- Schema Extensions: Added
permission_idsfield to team member profiles and team invitations in both client/server CRUD schemas, enabling storage and retrieval of permission arrays - API Enhancements: Modified team invitation endpoints (
/send-code,/accept) to handle optionalpermission_idsparameter, and added a new/role-permissionsendpoint to fetch available team permissions - Client Interface Updates: Extended team invitation methods (
sendTeamInvitation,inviteUser) to accept optionalpermissionIdsparameter and addedgetTeamRolePermissions()method for fetching available permissions - Backend Logic: Updated invitation acceptance handler to grant specific permissions when
permission_idsare provided, in addition to default team permissions - UI Components: Enhanced team member list to display roles as badges based on
permission_ids, and updated invitation form with role selection dropdown - Data Layer: Modified team member profile CRUD operations to fetch and return permission data by querying the
TeamMemberDirectPermissiontable
The implementation maintains backward compatibility by making permission_ids optional throughout, allowing existing functionality to continue working while enabling enhanced permission management. The changes follow established patterns in the codebase for CRUD operations, API design, and client-server communication. Comprehensive documentation has been added covering the new role-based invitation concepts, API usage, and migration guidance.
Confidence score: 2/5
- This PR introduces complex permission logic with several implementation inconsistencies that could cause production issues
- Score lowered due to schema conflicts, API documentation mismatches, and potential null safety issues in UI components
- Pay close attention to
packages/stack-shared/src/interface/crud/team-member-profiles.tsfor duplicate field definitions andapps/backend/src/app/api/latest/team-invitations/role-permissions/route.tsxfor behavior/documentation mismatch
19 files reviewed, 14 comments
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
packages/stack-shared/src/interface/crud/team-member-profiles.ts (2)
31-33: Docs grammar: fix “your are”“that your are a member” → “that you are a member”.
- description: "List team members profiles. You always need to specify a `team_id` that your are a member of on the client. You can always filter for your own profile by setting `me` as the `user_id` in the path parameters. If you want list all the profiles in a team, you need to have the `$read_members` permission in that team.", + description: "List team members profiles. You always need to specify a `team_id` that you are a member of on the client. You can filter for your own profile by setting `me` as the `user_id` in the path parameters. If you want to list all the profiles in a team, you need the `$read_members` permission in that team.",
41-43: Docs grammar/casingStart sentence with capital “You”.
- description: "Get a team member profile. you can always get your own profile by setting `me` as the `user_id` in the path parameters on the client. If you want to get someone else's profile in a team, you need to have the `$read_members` permission in that team.", + description: "Get a team member profile. You can always get your own profile by setting `me` as the `user_id` in the path parameters on the client. If you want to get someone else's profile in a team, you need the `$read_members` permission in that team.",apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx (1)
35-50: Privilege escalation risk: inviter can assign arbitrary permissionsThere’s no authorization check that the inviter is allowed to grant the requested
permission_ids. A user with only$invite_memberscould escalate invitees to admin by including powerful permissions. Ensure each requested permission is within the inviter’s effective permissions (or gate behind a stronger “assign” capability).await retryTransaction(prisma, async (tx) => { if (auth.type === "client") { if (!auth.user) throw new KnownErrors.UserAuthenticationRequired(); await ensureUserTeamPermissionExists(tx, { tenancy: auth.tenancy, userId: auth.user.id, teamId: body.team_id, permissionId: "$invite_members", errorType: 'required', recursive: true, }); + + // Optional: require an explicit assignment capability instead of subset check + // await ensureUserTeamPermissionExists(tx, { ...permissionId: "$assign_permissions", errorType: 'required', recursive: true }); + + // Ensure inviter can grant every requested permission + const grantIds = Array.from(new Set(body.permission_ids ?? [])); + for (const pid of grantIds) { + await ensureUserTeamPermissionExists(tx, { + tenancy: auth.tenancy, + userId: auth.user.id, + teamId: body.team_id, + permissionId: pid, + errorType: 'forbidden', + recursive: true, + }); + } } });
🧹 Nitpick comments (19)
packages/stack-shared/src/interface/server-interface.ts (1)
662-671: Nit: omit undefined in request body and keep header casing consistentUse filterUndefined to avoid serializing undefined, and match header casing used elsewhere.
Apply:
{ method: "POST", headers: { - "Content-Type": "application/json" + "content-type": "application/json" }, - body: JSON.stringify({ - email: options.email, - team_id: options.teamId, - callback_url: options.callbackUrl, - permission_ids: options.permissionIds, - }), + body: JSON.stringify(filterUndefined({ + email: options.email, + team_id: options.teamId, + callback_url: options.callbackUrl, + permission_ids: options.permissionIds, + })), },docs/templates/sdk/types/team-profile.mdx (1)
54-66: Clarify whether permissionIds are direct vs. effective permissions and link to role-permissionsAdd a brief note indicating if these are direct grants only (not inherited) and link to the role-permissions endpoint doc for mapping IDs to roles.
docs/templates/sdk/types/team.mdx (2)
207-213: Document the new permissionIds option in ParametersAdd a ParamField entry so readers see the option outside the signature.
Apply:
<ParamField path="callbackUrl" type="string"> The URL where users will be redirected after accepting the team invitation. Required when calling `inviteUser()` in the server environment since the URL cannot be automatically determined. Example: `https://your-app-url.com/handler/team-invitation` </ParamField> + <ParamField path="permissionIds" type="string[]"> + Optional list of permission IDs to grant upon acceptance. Omit to use the project's default role. + </ParamField>
324-326: Keep "Returns" text consistent with signature (include permissionIds)The Signature includes permissionIds, but the "Returns" text above still omits it.
Apply:
- `Promise<{ id: string, email: string, expiresAt: Date }[]>` + `Promise<{ id: string, email: string, expiresAt: Date, permissionIds: string[] }[]>`apps/backend/src/app/api/latest/team-invitations/crud.tsx (1)
56-57: Prefer nullish coalescing over logical OR for defaultingUse ?? to default only when undefined/null.
Apply:
- permission_ids: code.data.permission_ids || [], + permission_ids: code.data.permission_ids ?? [],packages/template/src/lib/stack-app/apps/interfaces/client-app.ts (1)
57-57: Ensure return shape aligns with backend payloadConfirm the backend route returns
contained_permission_idsandis_paginated: falseexactly as typed here, and consider promoting this response shape to a shared exported type in stack-shared to avoid drift.Apply this diff if you want to centralize the type:
- getTeamRolePermissions(): Promise<{ items: { id: string, description?: string, contained_permission_ids: string[] }[], is_paginated: false }>, + getTeamRolePermissions(): Promise<RolePermissionsList>,(With
RolePermissionsListintroduced under stack-shared and imported here.)packages/template/src/components-page/account-settings/teams/team-member-list-section.tsx (1)
24-52: Localize role labels and avoid raw IDs in UI
- Wrap “Admin”/“Member” with
t(...).- Avoid showing raw role IDs; prefer mapping via
getTeamRolePermissions()(id → human label), falling back tot("Member").- const userRoles = useMemo(() => { + const userRoles = useMemo(() => { const rolesMap = new Map<string, string>(); - for (const user of users) { // Use permission_ids directly from teamProfile const permissionIds = user.teamProfile.permission_ids || []; // Filter out $-prefixed permissions const filteredPermissions = permissionIds.filter((id: string) => !id.startsWith('$')); // Find matching role based to permission IDs - let roleName = "Member"; + let roleName = t("Member"); if (filteredPermissions.length > 0) { const roleId = filteredPermissions[0]; if (roleId === 'team_admin') { - roleName = "Admin"; + roleName = t("Admin"); } else if (roleId === 'team_member') { - roleName = "Member"; + roleName = t("Member"); } else { - roleName = roleId; + roleName = roleId; // TODO: map via getTeamRolePermissions() description/name if available } } rolesMap.set(user.id, roleName); } return rolesMap; - }, [users]); + }, [users, t]);If you want, I can wire in
getTeamRolePermissions()to replace the raw ID fallback.apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx (1)
52-57: Normalize permission_ids before embedding into the codeDedup to reduce redundant work downstream and keep payload minimal.
- const codeObj = await teamInvitationCodeHandler.sendCode({ + const codeObj = await teamInvitationCodeHandler.sendCode({ tenancy: auth.tenancy, data: { team_id: body.team_id, - permission_ids: body.permission_ids || [], + permission_ids: Array.from(new Set(body.permission_ids ?? [])), },apps/backend/src/app/api/latest/team-member-profiles/crud.tsx (3)
82-92: Eliminate N+1 permission queries.This does one findMany per member. Batch once and group by (teamId, projectUserId) to cut roundtrips and improve latency.
Example replacement:
- const permissions = await Promise.all(db.map(async (member) => { - const perms = await tx.teamMemberDirectPermission.findMany({ - where: { - tenancyId: auth.tenancy.id, - projectUserId: member.projectUserId, - teamId: member.teamId, - }, - select: { permissionId: true }, - }); - return perms.map(p => p.permissionId); - })); + const keys = db.map(m => `${m.teamId}:${m.projectUserId}`); + const permsAll = await tx.teamMemberDirectPermission.findMany({ + where: { + tenancyId: auth.tenancy.id, + projectUserId: { in: db.map(m => m.projectUserId) }, + teamId: { in: db.map(m => m.teamId) }, + }, + select: { permissionId: true, teamId: true, projectUserId: true }, + }); + const permsMap = new Map<string, string[]>(); + for (const p of permsAll) { + const k = `${p.teamId}:${p.projectUserId}`; + (permsMap.get(k) ?? permsMap.set(k, []).get(k)!).push(p.permissionId); + } + const permissions = keys.map(k => permsMap.get(k) ?? []);
94-99: Consider using user signup time as fallback, not membership createdAt.getUsersLastActiveAtMillis expects a per-user signup/time fallback. Passing teamMember.createdAt may skew “last active” for older users who joined a team later. Prefer projectUser.createdAt.
- const lastActiveAtMillis = await getUsersLastActiveAtMillis(auth.project.id, auth.branchId, db.map(user => user.projectUserId), db.map(user => user.createdAt)); + const lastActiveAtMillis = await getUsersLastActiveAtMillis( + auth.project.id, + auth.branchId, + db.map(u => u.projectUserId), + db.map(u => u.projectUser.createdAt), + );
21-23: Optional: stabilize permission_ids ordering.Sort permission_ids for deterministic output to reduce cache churn and flakey UI diffs.
- permission_ids: permissionIds, + permission_ids: [...permissionIds].sort(),docs/concepts/role-based-team-invitations.mdx (1)
7-12: Minor grammar/polish.
- “ability to:” → keep, but ensure each bullet is parallel (verb form).
- Consider “granular access control by letting you define exactly which permissions an invited user should have upon joining.”
packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx (3)
39-45: Reduce console noise and localize labels; minor cleanup.
- Drop dev console logs.
- Localize “Admin”/“Member” strings via t().
Apply:
- console.log('Fetching role permissions...'); const response = await stackApp.getTeamRolePermissions(); - console.log('Role permissions fetched:', response.items); setRolePermissions(response.items); @@ - console.log('No permissionIds provided, returning default'); return t("Default member role"); @@ - if (roleId === 'team_admin') return 'Admin'; - if (roleId === 'team_member') return 'Member'; + if (roleId === 'team_admin') return t('Admin'); + if (roleId === 'team_member') return t('Member'); @@ - if (firstPermissionId === 'team_admin') return 'Admin'; - if (firstPermissionId === 'team_member') return 'Member'; + if (firstPermissionId === 'team_admin') return t('Admin'); + if (firstPermissionId === 'team_member') return t('Member'); @@ - {permission.id === 'team_admin' ? 'Admin' : - permission.id === 'team_member' ? 'Member' : + {permission.id === 'team_admin' ? t('Admin') : + permission.id === 'team_member' ? t('Member') : permission.id}Also applies to: 55-57, 216-219, 76-79, 83-86
66-71: Micro-optimization: use Set for membership checks instead of a Map-to-self.Current roleMap maps id->id, then you .get to test presence. A Set is clearer.
Apply:
- // Map permission IDs to their IDs (instead of descriptions) - const roleMap = new Map(rolePermissions.map(role => [role.id, role.id])); + // Fast membership check of known role IDs + const roleIds = new Set(rolePermissions.map(role => role.id)); @@ - const matchingRoles = filteredPermissionIds.map(id => roleMap.get(id)).filter(Boolean); + const matchingRoles = filteredPermissionIds.filter(id => roleIds.has(id));
35-49: Duplicate fetching of role permissions; consider a shared hook or prop-drilling.Both components fetch the same data separately. Extract a useTeamRolePermissions() hook (with memoized cache) or fetch once in the parent and pass down.
I can sketch a small useTeamRolePermissions() hook backed by a simple in-memory cache to avoid duplicate network calls. Want me to add it?
Also applies to: 153-165
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (3)
656-657: Defensive default for permission_ids.If older backends omit permission_ids, this will be undefined and violate TeamMemberProfile’s string[] type. Default to [].
Apply:
- permission_ids: crud.permission_ids, + permission_ids: crud.permission_ids ?? [],
666-667: Match invitation shape defensively.Same rationale as above for invitations.
Apply:
- permissionIds: crud.permission_ids, + permissionIds: crud.permission_ids ?? [],
1438-1457: Consider lightweight validation/caching for role-permissions; drop unnecessary header.
- GET doesn’t need Content-Type.
- Optional: add a small cache to avoid refetching per component mount; or validate the expected shape to fail fast.
Apply minimal cleanup:
- { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }, + { method: "GET" },If you want, I can add a createCacheBySession for role-permissions and wire a useAsyncCache variant.
docs/templates-python/concepts/teams-management.mdx (1)
220-241: Docs align well; small clarity tweaks suggested.
- In send_team_invitation, explicitly note that omitting permission_ids (or passing None/empty) applies the default member role.
- In examples, mention that the valid IDs come from get_team_role_permissions to avoid guesswork.
Apply:
@@ - Send an invitation to join a team with optional role-based permissions + Send an invitation to join a team with optional role-based permissions. + If permission_ids is None or empty, the default member role is applied. @@ -# Example usage - Send invitation with admin role +# Example usage - Send invitation with admin role (IDs from get_team_role_permissions)Also applies to: 248-267, 298-323, 331-348, 456-488
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (19)
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx(3 hunks)apps/backend/src/app/api/latest/team-invitations/crud.tsx(1 hunks)apps/backend/src/app/api/latest/team-invitations/role-permissions/route.tsx(1 hunks)apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx(3 hunks)apps/backend/src/app/api/latest/team-member-profiles/crud.tsx(4 hunks)docs/concepts/role-based-team-invitations.mdx(1 hunks)docs/templates-python/concepts/teams-management.mdx(8 hunks)docs/templates/sdk/types/team-profile.mdx(2 hunks)docs/templates/sdk/types/team.mdx(4 hunks)packages/stack-shared/src/interface/client-interface.ts(2 hunks)packages/stack-shared/src/interface/crud/team-invitation.ts(1 hunks)packages/stack-shared/src/interface/crud/team-member-profiles.ts(1 hunks)packages/stack-shared/src/interface/server-interface.ts(2 hunks)packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx(4 hunks)packages/template/src/components-page/account-settings/teams/team-member-list-section.tsx(3 hunks)packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts(5 hunks)packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts(4 hunks)packages/template/src/lib/stack-app/apps/interfaces/client-app.ts(1 hunks)packages/template/src/lib/stack-app/teams/index.ts(4 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Prefer ES6 Map over Record where feasible
Files:
packages/template/src/lib/stack-app/apps/interfaces/client-app.tspackages/stack-shared/src/interface/crud/team-invitation.tsapps/backend/src/app/api/latest/team-invitations/crud.tsxpackages/stack-shared/src/interface/crud/team-member-profiles.tsapps/backend/src/app/api/latest/team-invitations/role-permissions/route.tsxapps/backend/src/app/api/latest/team-invitations/send-code/route.tsxpackages/template/src/components-page/account-settings/teams/team-member-list-section.tsxpackages/stack-shared/src/interface/server-interface.tspackages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsxpackages/stack-shared/src/interface/client-interface.tsapps/backend/src/app/api/latest/team-member-profiles/crud.tsxapps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsxpackages/template/src/lib/stack-app/teams/index.tspackages/template/src/lib/stack-app/apps/implementations/server-app-impl.tspackages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
apps/backend/src/app/api/latest/**
📄 CodeRabbit inference engine (CLAUDE.md)
Place backend API route handlers under /apps/backend/src/app/api/latest and follow RESTful, resource-based paths (auth, users, teams, oauth-providers)
Files:
apps/backend/src/app/api/latest/team-invitations/crud.tsxapps/backend/src/app/api/latest/team-invitations/role-permissions/route.tsxapps/backend/src/app/api/latest/team-invitations/send-code/route.tsxapps/backend/src/app/api/latest/team-member-profiles/crud.tsxapps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx
🧠 Learnings (1)
📚 Learning: 2025-08-24T18:36:37.712Z
Learnt from: CR
PR: stack-auth/stack-auth#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T18:36:37.712Z
Learning: Applies to apps/backend/src/app/api/latest/**/*.ts : Use the custom route handler system in the backend for consistent API responses
Applied to files:
apps/backend/src/app/api/latest/team-invitations/role-permissions/route.tsx
🧬 Code graph analysis (9)
packages/stack-shared/src/interface/crud/team-member-profiles.ts (1)
packages/stack-shared/src/interface/crud/users.ts (1)
usersCrudServerReadSchema(24-57)
apps/backend/src/app/api/latest/team-invitations/role-permissions/route.tsx (3)
apps/backend/src/route-handlers/smart-route-handler.tsx (1)
createSmartRouteHandler(209-294)packages/stack-shared/src/schema-fields.ts (7)
yupObject(247-251)clientOrHigherAuthTypeSchema(481-481)adaptSchema(330-330)yupNumber(191-194)yupString(187-190)yupArray(213-216)yupBoolean(195-198)apps/backend/src/lib/permissions.tsx (1)
listPermissionDefinitions(183-193)
apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx (1)
packages/stack-shared/src/schema-fields.ts (2)
yupArray(213-216)permissionDefinitionIdSchema(680-689)
packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx (2)
packages/template/src/lib/translations.tsx (1)
useTranslation(4-19)packages/stack-shared/src/schema-fields.ts (3)
yupObject(247-251)strictEmailSchema(448-448)yupString(187-190)
packages/stack-shared/src/interface/client-interface.ts (1)
packages/stack-shared/src/sessions.ts (1)
InternalSession(51-212)
apps/backend/src/app/api/latest/team-member-profiles/crud.tsx (1)
apps/backend/src/app/api/latest/users/crud.tsx (2)
getUsersLastActiveAtMillis(199-223)getUserLastActiveAtMillis(188-194)
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx (2)
packages/stack-shared/src/schema-fields.ts (2)
yupArray(213-216)permissionDefinitionIdSchema(680-689)apps/backend/src/lib/permissions.tsx (1)
grantTeamPermission(96-138)
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (1)
packages/template/src/utils/url.ts (1)
constructRedirectUrl(4-20)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)
packages/template/src/utils/url.ts (1)
constructRedirectUrl(4-20)
🪛 LanguageTool
docs/concepts/role-based-team-invitations.mdx
[grammar] ~7-~7: There might be a mistake here.
Context: ...nvitation flow by adding the ability to: - Select specific role-based permissions d...
(QB_NEW_EN)
[grammar] ~162-~162: There might be a mistake here.
Context: ...e.g., $update_team, $invite_members) - Role-Based Permissions: Custom permiss...
(QB_NEW_EN)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Security Check
🔇 Additional comments (16)
packages/stack-shared/src/interface/crud/team-invitation.ts (1)
10-10: Addition of permission_ids to client read schema — LGTMShape aligns with backend list response; required array with empty fallback is appropriate.
packages/stack-shared/src/interface/server-interface.ts (1)
653-659: Public API: added optional permissionIds to sendServerTeamInvitationSignature change looks good and matches docs/UI updates.
docs/templates/sdk/types/team-profile.mdx (1)
21-22: Add permissionIds to ToC — LGTMdocs/templates/sdk/types/team.mdx (1)
352-359: useInvitations return shape updated — LGTMapps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx (1)
110-129: Should invitations add permissions for existing members?Currently, permissions are only applied when a new membership is created. If a user is already a member, the invitation silently skips grants. Confirm this matches product expectations.
Would you like a follow-up change to apply (or merge)
permission_idsfor existing members too?packages/template/src/components-page/account-settings/teams/team-member-list-section.tsx (1)
71-92: LGTM: role column renderingThe new Role column and badge rendering are straightforward and readable.
apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx (1)
24-25: Schema addition looks goodAccepting optional
permission_idswithpermissionDefinitionIdSchemamatches shared constraints.apps/backend/src/app/api/latest/team-member-profiles/crud.tsx (1)
15-24: Output shape extension looks good.Adding permission_ids to prismaToCrud keeps naming consistent with API contracts.
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (3)
603-604: LGTM: surfaced permission_ids on teamProfile.Keeps server model aligned with CRUD response.
613-617: LGTM: invitations expose permissionIds.Matches shared interface semantics.
674-680: Verify server interface compatibility for permissionIds.Confirm StackServerInterface.sendServerTeamInvitation({ permissionIds }) exists and is wired to backend send-code route expecting permission_ids.
Run a quick grep to confirm the method signature and payload mapping exist in server interface and transport layer.
packages/stack-shared/src/interface/client-interface.ts (1)
690-709: LGTM: sendTeamInvitation propagates permissionIds → permission_ids.apps/backend/src/app/api/latest/team-invitations/role-permissions/route.tsx (1)
1-3: Good use of Smart Route Handler per backend guidelines.docs/concepts/role-based-team-invitations.mdx (1)
35-68: Ensure examples match API output.If backend filters out $permissions, remove those entries from the example payload; otherwise keep as-is.
packages/template/src/lib/stack-app/teams/index.ts (1)
13-14: All inviteUser call sites now use the object signature – every invocation passes an options object with email, optional callbackUrl, and optional permissionIds as required.packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)
733-741: LGTM: inviteUser forwards permissionIds correctly and refreshes cache.No issues; consistent with the new public API.
Documentation Changes RequiredBased on the recent pull request, the following documentation changes are needed: 1. docs/templates/sdk/types/team.mdx
2. docs/templates/sdk/types/team-profile.mdx
3. docs/templates/sdk/objects/stack-app.mdx
4. docs/templates-python/concepts/teams-management.mdxUpdate the Python documentation for team invitations:
Please ensure these changes are reflected in the relevant documentation files to accurately represent the new features and modifications introduced by the pull request. |
…s API - Add permission_ids field to team-member-profiles schema (client & server) - Update CRUD operations to fetch and include direct permission_ids - Fix team-member-list-section to use permission_ids for role display - Optimize permission fetching to avoid N+1 queries - Add atomic permission granting in team-invitations/accept - Add e2e test coverage for permission_ids in API responses - Fix naming convention violations (snake_case to camelCase) - Remove debug console.log statements Resolves broken member list roles functionality
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx (1)
100-131: Always grantpermission_idswhen accepting invites, even for existing members
Currently, permission grants are scoped to the!oldMembershipbranch, so re-invited members with newpermission_idsreceive no updates. Move the membership check inside the transaction and apply grants unconditionally to avoid this gap and eliminate the TOCTOU window:@@ apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx - const oldMembership = await prisma.teamMember.findUnique({ … }); - - if (!oldMembership) { - await retryTransaction(prisma, async (tx) => { - await teamMembershipsCrudHandlers.adminCreate({ tenancy, team_id: data.team_id, user_id: user.id, data: {} }); - - if (data.permission_ids?.length) { - const uniquePermissionIds = [...new Set(data.permission_ids)]; - for (const permissionId of uniquePermissionIds) { - await grantTeamPermission(tx, { tenancy, teamId: data.team_id, userId: user.id, permissionId }); - } - } - }); - } + await retryTransaction(prisma, async (tx) => { + const existing = await tx.teamMember.findUnique({ + where: { tenancyId_projectUserId_teamId: { tenancyId: tenancy.id, projectUserId: user.id, teamId: data.team_id } }, + }); + if (!existing) { + await teamMembershipsCrudHandlers.adminCreate({ tenancy, team_id: data.team_id, user_id: user.id, data: {} }); + } + if (data.permission_ids?.length) { + const uniquePermissionIds = Array.from(new Set(data.permission_ids)); + for (const permissionId of uniquePermissionIds) { + await grantTeamPermission(tx, { tenancy, teamId: data.team_id, userId: user.id, permissionId }); + } + } + });apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts (5)
151-174: Update snapshot to include permission_ids.API now returns permission_ids on member profiles.
@@ expect(response2).toMatchInlineSnapshot(` NiceResponse { "status": 200, "body": { "is_paginated": false, "items": [ { "display_name": "User 1", + "permission_ids": [], "profile_image_url": null, "team_id": "<stripped UUID>", "user_id": "<stripped UUID>", }, ], }, "headers": Headers { <some fields may have been hidden> }, } `);
191-219: Avoid brittle list snapshot; assert structure including permission_ids.Permissions vary by member. Replace the inline snapshot with structural checks.
- expect(response3).toMatchInlineSnapshot(` - NiceResponse { - "status": 200, - "body": { - "is_paginated": false, - "items": [ - { - "display_name": "User 3 (team creator)", - "profile_image_url": null, - "team_id": "<stripped UUID>", - "user_id": "<stripped UUID>", - }, - { - "display_name": "User 1", - "profile_image_url": null, - "team_id": "<stripped UUID>", - "user_id": "<stripped UUID>", - }, - { - "display_name": "User 2", - "profile_image_url": null, - "team_id": "<stripped UUID>", - "user_id": "<stripped UUID>", - }, - ], - }, - "headers": Headers { <some fields may have been hidden> }, - } - `); + expect(response3.status).toBe(200); + expect(response3.body.is_paginated).toBe(false); + expect(Array.isArray(response3.body.items)).toBe(true); + for (const item of response3.body.items) { + expect(item).toEqual( + expect.objectContaining({ + display_name: expect.any(String), + team_id: expect.any(String), + user_id: expect.any(String), + permission_ids: expect.any(Array), + }) + ); + }
229-240: Update update-profile snapshot to include permission_ids.expect(response4).toMatchInlineSnapshot(` NiceResponse { "status": 200, "body": { "display_name": "Team Member Name Updated", + "permission_ids": [], "profile_image_url": null, "team_id": "<stripped UUID>", "user_id": "<stripped UUID>", }, "headers": Headers { <some fields may have been hidden> }, } `);
251-266: Update read-self snapshot to include permission_ids.expect(response5).toMatchInlineSnapshot(` NiceResponse { "status": 200, "body": { "display_name": "Team Member Name Updated", + "permission_ids": [], "profile_image_url": null, "team_id": "<stripped UUID>", "user_id": "<stripped UUID>", }, "headers": Headers { <some fields may have been hidden> }, } `);
297-308: Avoid brittle read-other snapshot; assert structure including permission_ids.User 2’s permissions may differ; prefer structural assertions.
- expect(response7).toMatchInlineSnapshot(` - NiceResponse { - "status": 200, - "body": { - "display_name": "User 2 Updated", - "profile_image_url": null, - "team_id": "<stripped UUID>", - "user_id": "<stripped UUID>", - }, - "headers": Headers { <some fields may have been hidden> }, - } - `); + expect(response7.status).toBe(200); + expect(response7.body).toEqual( + expect.objectContaining({ + display_name: "User 2 Updated", + team_id: expect.any(String), + user_id: expect.any(String), + permission_ids: expect.any(Array), + }) + );
♻️ Duplicate comments (1)
apps/backend/src/app/api/latest/team-member-profiles/crud.tsx (1)
206-215: Fix wrong user id field in getUserLastActiveAtMillis call.Use db.projectUserId, not db.projectUser.projectUserId.
- return prismaToCrud(db, await getUserLastActiveAtMillis(auth.project.id, auth.branchId, db.projectUser.projectUserId) ?? db.projectUser.createdAt.getTime(), perms.map(p => p.permissionId)); + return prismaToCrud( + db, + (await getUserLastActiveAtMillis(auth.project.id, auth.branchId, db.projectUserId)) ?? db.projectUser.createdAt.getTime(), + perms.map(p => p.permissionId) + );
🧹 Nitpick comments (3)
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx (1)
33-35: Validate team_id as UUID for consistency.Other routes use uuid() for team_id; align the schema here.
- data: yupObject({ - team_id: yupString().defined(), + data: yupObject({ + team_id: yupString().uuid().defined(), permission_ids: yupArray(permissionDefinitionIdSchema.defined()).optional(), }).defined(),apps/backend/src/app/api/latest/team-member-profiles/crud.tsx (2)
160-172: Align Read path with new helper keying.Use the same team+user key to avoid future regressions.
- const permissionMap = await fetchTeamMemberPermissions( - tx, - auth.tenancy.id, - db.teamId, - [db.projectUserId] - ); - - return prismaToCrud( - db, - await getUserLastActiveAtMillis(auth.project.id, auth.branchId, db.projectUserId) ?? db.projectUser.createdAt.getTime(), - permissionMap.get(db.projectUserId) || [] - ); + const permissionMap = await fetchTeamMemberPermissions(tx, auth.tenancy.id, [{ teamId: db.teamId, projectUserId: db.projectUserId }]); + const key = `${db.teamId}::${db.projectUserId}`; + return prismaToCrud( + db, + (await getUserLastActiveAtMillis(auth.project.id, auth.branchId, db.projectUserId)) ?? db.projectUser.createdAt.getTime(), + permissionMap.get(key) ?? [] + );
206-213: DRY: reuse fetchTeamMemberPermissions in Update path.Avoid duplicating permission queries.
- const perms = await tx.teamMemberDirectPermission.findMany({ - where: { - tenancyId: auth.tenancy.id, - projectUserId: db.projectUserId, - teamId: db.teamId, - }, - select: { permissionId: true }, - }); - - return prismaToCrud(db, await getUserLastActiveAtMillis(auth.project.id, auth.branchId, db.projectUser.projectUserId) ?? db.projectUser.createdAt.getTime(), perms.map(p => p.permissionId)); + const pmap = await fetchTeamMemberPermissions(tx, auth.tenancy.id, [{ teamId: db.teamId, projectUserId: db.projectUserId }]); + const key = `${db.teamId}::${db.projectUserId}`; + return prismaToCrud( + db, + (await getUserLastActiveAtMillis(auth.project.id, auth.branchId, db.projectUserId)) ?? db.projectUser.createdAt.getTime(), + pmap.get(key) ?? [] + );
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (8)
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx(3 hunks)apps/backend/src/app/api/latest/team-member-profiles/crud.tsx(4 hunks)apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts(1 hunks)packages/stack-shared/src/interface/crud/team-member-profiles.ts(1 hunks)packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx(4 hunks)packages/template/src/components-page/account-settings/teams/team-member-list-section.tsx(3 hunks)packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts(5 hunks)packages/template/src/lib/stack-app/teams/index.ts(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
- packages/stack-shared/src/interface/crud/team-member-profiles.ts
- packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
- packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx
- packages/template/src/components-page/account-settings/teams/team-member-list-section.tsx
- packages/template/src/lib/stack-app/teams/index.ts
🧰 Additional context used
📓 Path-based instructions (4)
apps/e2e/**
📄 CodeRabbit inference engine (CLAUDE.md)
Always add new E2E tests when you change the API or SDK interface
Files:
apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts
**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
In tests, prefer .toMatchInlineSnapshot where possible
Files:
apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Prefer ES6 Map over Record where feasible
Files:
apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.tsapps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsxapps/backend/src/app/api/latest/team-member-profiles/crud.tsx
apps/backend/src/app/api/latest/**
📄 CodeRabbit inference engine (CLAUDE.md)
Place backend API route handlers under /apps/backend/src/app/api/latest and follow RESTful, resource-based paths (auth, users, teams, oauth-providers)
Files:
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsxapps/backend/src/app/api/latest/team-member-profiles/crud.tsx
🧬 Code graph analysis (3)
apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts (1)
apps/e2e/tests/backend/backend-helpers.ts (1)
niceBackendFetch(107-166)
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx (4)
packages/stack-shared/src/schema-fields.ts (2)
yupArray(213-216)permissionDefinitionIdSchema(680-689)apps/backend/src/prisma-client.tsx (1)
retryTransaction(124-193)apps/backend/src/app/api/latest/team-memberships/crud.tsx (1)
teamMembershipsCrudHandlers(43-160)apps/backend/src/lib/permissions.tsx (1)
grantTeamPermission(96-138)
apps/backend/src/app/api/latest/team-member-profiles/crud.tsx (1)
apps/backend/src/app/api/latest/users/crud.tsx (2)
getUsersLastActiveAtMillis(199-223)getUserLastActiveAtMillis(188-194)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Security Check
🔇 Additional comments (1)
apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts (1)
311-322: LGTM: new test covers permission_ids presence.Good addition aligning with API changes.
- Changed the return statement in the `get` method to use `await res.json()` instead of `res.data` for proper JSON handling. This change ensures that the API response is correctly parsed as JSON, improving data handling in the client interface.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)
1504-1523: Return camelCase from getTeamRolePermissions and map the payloadLeaking snake_case (contained_permission_ids, is_paginated) from HTTP breaks JS client conventions and prior guidance. Map to containedPermissionIds and isPaginated.
- async getTeamRolePermissions(): Promise<{ items: { id: string, description?: string, contained_permission_ids: string[] }[], is_paginated: false }> { + async getTeamRolePermissions(): Promise<{ items: { id: string; description?: string; containedPermissionIds: string[] }[]; isPaginated: false }> { const session = await this._getSession(); const result = await this._interface.sendClientRequest( "/team-invitations/role-permissions", { method: "GET", headers: { - "Content-Type": "application/json", + "Accept": "application/json", }, }, session ); if (!result.ok) { throw new Error(`Failed to get team role permissions: ${result.status} ${await result.text()}`); } - const data = await result.json(); - return data; + const json = await result.json() as { items: { id: string; description?: string; contained_permission_ids: string[] }[]; is_paginated: false }; + return { + items: json.items.map(({ id, description, contained_permission_ids }) => ({ + id, + description, + containedPermissionIds: contained_permission_ids, + })), + isPaginated: false, + }; }
🧹 Nitpick comments (3)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (3)
657-665: Default permissionIds to an empty array to avoid undefined at call sitesSafer for consumers and UI mapping.
teamProfile: { displayName: crud.display_name, profileImageUrl: crud.profile_image_url, - permissionIds: crud.permission_ids, + permissionIds: crud.permission_ids ?? [], }
668-676: Also default invitation.permissionIds to []Keeps the client shape stable even when backend omits the field.
expiresAt: new Date(crud.expires_at_millis), - permissionIds: crud.permission_ids, + permissionIds: crud.permission_ids ?? [],
1511-1513: Use Accept for GET; Content-Type is unnecessary without a bodyMinor HTTP header nit.
- headers: { - "Content-Type": "application/json", - }, + headers: { "Accept": "application/json" },
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
packages/stack-shared/src/interface/client-interface.ts(2 hunks)packages/stack-shared/src/interface/server-interface.ts(2 hunks)packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts(4 hunks)packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/stack-shared/src/interface/server-interface.ts
- packages/stack-shared/src/interface/client-interface.ts
- packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts
🧰 Additional context used
📓 Path-based instructions (2)
packages/template/**
📄 CodeRabbit inference engine (AGENTS.md)
When modifying the SDK copies, make changes in packages/template (source of truth)
Files:
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Prefer ES6 Map over Record when representing key–value collections
Files:
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
🧬 Code graph analysis (1)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)
packages/template/src/utils/url.ts (1)
constructRedirectUrl(4-20)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Security Check
🔇 Additional comments (1)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)
740-747: No action — sendTeamInvitation already maps permissionIds → permission_idspackages/stack-shared/src/interface/client-interface.ts defines sendTeamInvitation(options: { permissionIds?: string[] }) and sends permission_ids: options.permissionIds; server-interface does the same. No change required.
|
Hi @bootssecurity, we're looking to merge this pr. Can you make these adjustments?
|
…n_ids validation - Added tests for fetching role permissions from the role-permissions endpoint. - Implemented tests for sending invitations with specific permission_ids and verifying their application upon acceptance. - Updated team member profile tests to ensure permission_ids are included in responses for both the inviting and invited users. - Refactored permission fetching logic in the frontend to utilize the new project.listTeamPermissionDefinitions method.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (1)
559-579: Strengthen assertions and add edge case coverage.The test validates basic response structure but could be more thorough:
- The conditional check at line 573 means the test passes with zero permissions, leaving structure assertions unverified.
- No validation of actual permission IDs or values returned.
- Missing error scenarios (e.g., unauthenticated requests, server access type behavior).
Consider these improvements:
it("can fetch role permissions from role-permissions endpoint", async ({ expect }) => { await Auth.Otp.signIn(); const response = await niceBackendFetch("/api/v1/team-invitations/role-permissions", { accessType: "client", method: "GET", }); expect(response.status).toBe(200); expect(response.body).toHaveProperty('items'); expect(response.body).toHaveProperty('is_paginated', false); expect(Array.isArray(response.body.items)).toBe(true); + expect(response.body.items.length).toBeGreaterThan(0); - // Check that each item has the correct structure - if (response.body.items.length > 0) { - const item = response.body.items[0]; + // Validate every item has the correct structure + for (const item of response.body.items) { expect(item).toHaveProperty('id'); + expect(typeof item.id).toBe('string'); + expect(item.id.length).toBeGreaterThan(0); expect(item).toHaveProperty('contained_permission_ids'); expect(Array.isArray(item.contained_permission_ids)).toBe(true); } });Additionally, consider adding tests for:
- Unauthenticated access returning 401
- Server access type behavior
- Validation that known permission IDs (e.g., "team_admin") are present in the response
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts(1 hunks)apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts
🧰 Additional context used
🧬 Code graph analysis (1)
apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (3)
apps/e2e/tests/backend/backend-helpers.ts (3)
niceBackendFetch(109-173)createMailbox(59-66)backendContext(35-57)packages/template/src/lib/stack-app/projects/index.ts (1)
Project(10-15)packages/template/src/lib/stack-app/teams/index.ts (1)
Team(38-52)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Vercel Agent Review
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
- Introduced tests for handling invalid, malformed, and mixed permission_ids in team invitations, ensuring proper error responses. - Added tests for permission escalation scenarios, verifying that users without invite permissions cannot grant elevated permissions. - Implemented tests for backwards compatibility with empty and omitted permission_ids. - Included server access tests to confirm that invitations can be sent with permission_ids bypassing user permissions. - Enhanced role-permissions endpoint tests for consistent responses between client and server access.
- Updated logic to ensure that only non-default roles are included in permissionIds when inviting users to a team, enhancing the accuracy of permission assignments during the invitation process.
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (3)
packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx (3)
40-42: Remove debug console.log statements.These debug statements should be removed before merging to production.
Apply this diff:
const fetchRolePermissions = async () => { try { - console.log('Fetching role permissions...'); const permissions = await project.listTeamPermissionDefinitions(); - console.log('Role permissions fetched:', permissions); setRolePermissions(permissions); } catch (error) {
144-144: Remove duplicate watch destructuring.
watchFormis destructured but never used in the code, andwatchis already destructured earlier in the same statement.Apply this diff:
- const { register, handleSubmit, formState: { errors }, watch, setValue, watch: watchForm } = useForm({ + const { register, handleSubmit, formState: { errors }, watch, setValue } = useForm({
211-225: Make Select controlled to enable visual reset.The Select component is currently uncontrolled (missing
valueprop), so callingsetValue('role', '')after form submission doesn't reset the visual selection. Users will see the previously selected role still displayed after inviting a member.Apply this diff to make the Select controlled:
- <Select onValueChange={(value) => setValue('role', value)}> + <Select value={watch('role') ?? 'default'} onValueChange={(value) => setValue('role', value)}> <SelectTrigger>And update the default value and reset logic:
defaultValues: { email: '', - role: '', + role: 'default', }// Reset form setValue('email', ''); - setValue('role', ''); + setValue('role', 'default');
🧹 Nitpick comments (2)
packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx (2)
65-86: Simplify getRoleDisplayName logic.The current implementation creates a redundant Map that maps
role.idtorole.id, then performs lookups that simply return the original ID, followed by manual string comparisons for friendly names. This can be simplified significantly.Apply this diff to streamline the logic:
const getRoleDisplayName = (permissionIds: string[]) => { - if (permissionIds.length === 0) { - return t("Default member role"); - } - // Filter out permission IDs that start with $ const filteredPermissionIds = permissionIds.filter(id => !id.startsWith('$')); if (filteredPermissionIds.length === 0) { return t("Default member role"); } - // Map permission IDs to their IDs (instead of descriptions) - const roleMap = new Map(rolePermissions.map(role => [role.id, role.id])); - - - // Find the role that matches the permission IDs and return the ID - const matchingRoles = filteredPermissionIds.map(id => roleMap.get(id)).filter(Boolean); - - - if (matchingRoles.length > 0) { - const roleId = matchingRoles[0]; - // Map common roles to friendly names - if (roleId === 'team_admin') return 'Admin'; - if (roleId === 'team_member') return 'Member'; - return roleId; - } - - // Fallback to permission ID with mapping - const firstPermissionId = filteredPermissionIds[0]; - if (firstPermissionId === 'team_admin') return 'Admin'; - if (firstPermissionId === 'team_member') return 'Member'; - return firstPermissionId; + // Map common roles to friendly names + const roleId = filteredPermissionIds[0]; + if (roleId === 'team_admin') return 'Admin'; + if (roleId === 'team_member') return 'Member'; + return roleId; };
155-168: Consider extracting role permissions fetching to a custom hook.The role permissions fetching logic is duplicated in two places (lines 37-52 and here). Extracting it to a custom hook like
useTeamRolePermissions()would improve maintainability and reduce code duplication.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts(1 hunks)packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx(4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-11T04:13:19.308Z
Learnt from: N2D4
Repo: stack-auth/stack-auth PR: 943
File: examples/convex/app/action/page.tsx:23-28
Timestamp: 2025-10-11T04:13:19.308Z
Learning: In the stack-auth codebase, use `runAsynchronouslyWithAlert` from `stackframe/stack-shared/dist/utils/promises` for async button click handlers and form submissions instead of manual try/catch blocks. This utility automatically handles errors and shows alerts to users.
Applied to files:
packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx
🧬 Code graph analysis (2)
packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx (2)
packages/template/src/lib/translations.tsx (1)
useTranslation(4-19)packages/stack-shared/src/schema-fields.ts (3)
yupObject(247-251)strictEmailSchema(448-448)yupString(187-190)
apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (3)
apps/e2e/tests/backend/backend-helpers.ts (3)
niceBackendFetch(109-173)createMailbox(59-66)backendContext(35-57)packages/template/src/lib/stack-app/projects/index.ts (1)
Project(10-15)packages/template/src/lib/stack-app/teams/index.ts (1)
Team(38-52)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Vercel Agent Review
🔇 Additional comments (2)
apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (2)
559-579: LGTM: Role-permissions endpoint test is well-structured.The test correctly validates the response structure and properties for the new role-permissions endpoint.
581-663: LGTM: Happy path tests for permission_ids are thorough.Both tests correctly validate the full flow: sending invitations with permission_ids and verifying they are applied upon acceptance.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
- Updated test cases to use `receiveMailbox.emailAddress` for email field in team invitation requests. - Simplified error response assertions by removing unnecessary nested property checks for 'error', focusing directly on 'code' and 'message' properties. - Enhanced consistency in error handling across various invitation scenarios, ensuring clearer validation of permission-related errors.
|
Moved here to fix tests: |
Important
Add
permission_idssupport to team member profiles and invitations, enhancing role-based access control.permission_idsto team member profiles and invitations inteam-member-profiles/crud.tsandteam-invitation/crud.ts.server-interface.tsandclient-interface.tsto handlepermission_ids.role-permissionsendpoint inrole-permissions/route.tsxto fetch available permissions.team-member-invitation-section.tsxandteam-member-list-section.tsxto display roles based onpermission_ids.teams-management.mdxandrole-based-team-invitations.mdxwithpermission_idsexamples and type definitions.getTeamRolePermissions()toclient-app-impl.tsandserver-app-impl.tsfor fetching role permissions.team-profile.mdxandteam.mdxto includepermission_ids.This description was created by
for cf8e76b. You can customize this summary. It will automatically update as commits are pushed.
Summary by CodeRabbit
New Features
Behavioral
Documentation
Tests